#
# Copyright 2009 Red Hat, Inc. and/or its affiliates.
#
# Licensed to you under the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.  See the files README and
# LICENSE_GPL_v2 which accompany this distribution.
#


"""
multipath module provides helper procedures for configuring multipath
daemon and maintaining its state
"""
import glob
import os
import stat
import tempfile
import logging
import re

import constants
import misc
import iscsi

import storage_exception as se

DEV_ISCSI = "iSCSI"
DEV_FCP = "FCP"
DEV_MIXED = "MIXED"

MAX_CONF_COPIES = 5

TOXIC_CHARS = '()*+?|^$.\\'

MPATH_CONF = "/etc/multipath.conf"

OLD_TAGS = [ "# RHAT REVISION 0.2", "# RHEV REVISION 0.3",
             "# RHEV REVISION 0.4", "# RHEV REVISION 0.5", ]
MPATH_CONF_TAG = "# RHEV REVISION 0.6"
MPATH_CONF_PRIVATE_TAG = "# RHEV PRIVATE"
MPATH_CONF_TEMPLATE = MPATH_CONF_TAG + constants.STRG_MPATH_CONF

log = logging.getLogger("irs")

SCAN_PATTERN = "/sys/class/scsi_host/host*/scan"

def rescan():
    """
    Forces multiupath daemon to rescan the list of available devices and
    refresh the mapping table. New devices can be found under /dev/mapper

    Should only be called from hsm._rescanDevices()
    """

    # First ask iSCSI to rescan all its sessions
    iscsi.rescan()

    # Second rescan any newly added LU on all the initiators
    # Yes, it is redundant WRT the iSCSI, but KISS
    for host in glob.glob(SCAN_PATTERN):
        try:
            misc.writefileSUDO(host, "- - -")
        except Exception, e:
            # Ignore exception, there is nothing intelligent we can do about it
            log.warning("Failed to rescan host %s", host, exc_info=True)

    # Now let multipath daemon pick up new devices
    misc.execCmd([constants.EXT_MULTIPATH])


def isEnabled():
    """
    Check the multipath daemon configuration. The configuration file
    /etc/multipath.conf should contain private tag in form
    "RHEV REVISION X.Y" for this check to succeed.
    If the tag above is followed by tag "RHEV PRIVATE" the configuration
    should be preserved at all cost.

    """
    if os.path.exists(MPATH_CONF):
        first = second = ''
        mpathconf = misc.readfileSUDO(MPATH_CONF)
        try:
            first = mpathconf[0]
            second = mpathconf[1]
        except IndexError:
            pass
        if MPATH_CONF_PRIVATE_TAG in second:
            log.info("Manual override for multipath.conf detected - "
                "preserving current configuration")
            if MPATH_CONF_TAG not in first:
                log.warning("This manual override for multipath.conf was based "
                    "on downrevved template. You are strongly advised to "
                    "contact your support representatives")
            return True

        if MPATH_CONF_TAG in first:
            log.debug("Current revision of multipath.conf detected, preserving")
            return True

        for tag in OLD_TAGS:
            if tag in first:
                log.info("Downrev multipath.conf detected, upgrade required")
                return False

    log.debug("multipath Defaulting to False")
    return False

def deduceType(a, b):
    if a == b:
        return a
    else:
        return DEV_MIXED

def setupMultipath():
    """
    Set up the multipath daemon configuration to the known and
    supported state. The original configuration, if any, is saved
    """
    if os.path.exists(MPATH_CONF):
        misc.rotateFiles(os.path.dirname(MPATH_CONF),os.path.basename(MPATH_CONF), MAX_CONF_COPIES, cp=True, persist=True)
    f = tempfile.NamedTemporaryFile()
    f.write(MPATH_CONF_TEMPLATE)
    f.flush()
    cmd = [constants.EXT_CP, f.name, MPATH_CONF]
    rc = misc.execCmd(cmd)[0]
    if rc != 0:
        raise se.MultipathSetupError()
    # f close removes file - must be after copy
    f.close()
    misc.persistFile(MPATH_CONF)

    cmd = [constants.EXT_SERVICE, "multipathd", "restart"]
    rc = misc.execCmd(cmd)[0]
    if rc != 0:
        # No dice - try to reload instead of restart
        cmd = [constants.EXT_SERVICE, "multipathd", "reload"]
        rc = misc.execCmd(cmd)[0]
        if rc != 0:
            raise se.MultipathRestartError()

def getSerial(physdev):
    blkdev = os.path.join("/block", physdev)
    cmd = [constants.EXT_SCSI_ID, "-p", "0x80", "-guxs", blkdev]
    (rc, out, err) = misc.execCmd(cmd)
    if rc == 0:
        for line in out:
            if line.startswith("ID_SERIAL="):
                return line.split("=")[1]
    return ""

def getMpathDeviceCapacity():
    sizes = {}
    partFile = open("/proc/partitions", "r")
    for l in partFile:
        try:
            major, minor, size, name = l.split()

            if not name.startswith("dm"):
                continue

            sizes[name] = int(size) *1024
        except ValueError:
            continue

    return sizes

MULTIPATH_DEVICE_HEADER = re.compile(r"^\s*(?P<guid>[^\s]*)\s+(?P<dm>dm-[\d]+)\s+(?P<vendor>[^,]*?)\s*,\s*(?P<product>[^.]*?)\s*$")
MULTIPATH_PATH_LINE = re.compile(r"^\s*\\_\s*[^\s*]+\s+\d+\s+\[prio=\d+\]\[\w+\]\s*$")
MULTIPATH_DEVICE_LINE = re.compile(r"^\s*\\_\s*\d+:\d+:\d+:(?P<lun>\d+)\s+(?P<physdev>\w+)\s+\d+:\d+\s+\[(?P<state>\w+)\]\s*\[\w+\]\s*$")
def getStatus(filter=None):
    results = {}
    knownSessions = {}
    sizes = getMpathDeviceCapacity()
    cmd = [constants.EXT_MULTIPATH, "-ll"]
    (rc, out, err) = misc.execCmd(cmd)
    if rc:
        return results

    i = 0
    outLength = len(out)
    while i < outLength:
        match = MULTIPATH_DEVICE_HEADER.match(out[i])
        if match is None:
            i += 1
            continue

        devHeader = match.groupdict()
        guid = devHeader["guid"]
        if filter is not None and guid not in filter:
            i += 1
            continue

        results[guid] = {  "vendor" : devHeader["vendor"],
                           "product" : devHeader["product"],
                           "serial" : "",
                           "devtype" : [],
                           "connections" : [],
                           "paths" : [],
                           "size" : str(sizes.get(devHeader["dm"], "0"))
                         }
        # Skip the next line
        # it contains nothing of importance
        i += 2
        while True:
            if i >= outLength:
                break

            match = MULTIPATH_PATH_LINE.match(out[i])
            if match is None:
                break
            i += 1
            while True:
                if i >= outLength:
                    break

                match = MULTIPATH_DEVICE_LINE.match(out[i])
                if match is None:
                    break

                devLine = match.groupdict()
                physdev = devLine["physdev"]
                if not os.path.exists(os.path.join("/dev", physdev)):
                    log.warning("(pathinfo) no such physdev %s: '%s' is ignored" % (physdev, out[i]))
                    continue

                results[guid]["paths"].append(devLine)

                if iscsi.devIsiSCSI(physdev):
                    results[guid]["devtype"].append(DEV_ISCSI)
                    sessionID = iscsi.getiScsiSession(physdev)
                    if sessionID not in knownSessions:
                        knownSessions[sessionID] = iscsi.getdeviSCSIinfo(physdev)
                    results[guid]["connections"].append(knownSessions[sessionID])
                else:
                    results[guid]["devtype"].append(DEV_FCP)

                if results[guid]["serial"] == "":
                    results[guid]["serial"] = getSerial(physdev)

                i += 1

        if results[guid]["devtype"]:
            results[guid]["devtype"] = reduce(deduceType, results[guid]["devtype"])

    return results

def pathinfo(guid):
    allPaths = getStatus(filter=[guid])
    myPathInfo = allPaths.get(guid, None)
    if myPathInfo is None:
        return "", "", "", [], [], []

    return (myPathInfo["vendor"], myPathInfo["product"],
           myPathInfo["serial"], myPathInfo["devtype"],
           myPathInfo["connections"], myPathInfo["paths"])

def getMPDevNames():
    """
    Collect the list of all the multipath block devices.
    Return the list of device identifiers w/o "/dev/mapper" prefix
    """
    cmd = [constants.EXT_DMSETUP, "ls", "--target", "multipath"]
    rc, out, err = misc.execCmd(cmd)

    guids = [i.split()[0] for i in out]

    # Double check block devices are exist
    devs = []
    for guid in guids:
    # Filter out devices with toxic characters in their names/GUIDs
        if len([i for i in TOXIC_CHARS if i in guid]) == 0:
            blkdev = os.path.join("/dev/mapper", guid)
            try:
                if stat.S_ISBLK(os.stat(blkdev).st_mode):
                    devs.append(guid)
            except OSError:
                log.info("Multipath device %s doesn't exist", blkdev)
        else:
            log.info("Device with unsupported GUID %s discarded", guid)

    return devs

def isPartitioned(devPath):
    return len(glob.glob(devPath + "p*")) > 0

def devIsiSCSI(type):
    return type in [DEV_ISCSI, DEV_MIXED]


def devIsFCP(type):
    return type in [DEV_FCP, DEV_MIXED]

